// Copyright (c) Microsoft Corporation. All rights reserved.

using System;
using System.Collections.Generic;
using System.Collections;
using System.Text;
using Microsoft.TeamFoundation.Migration.Toolkit;
using System.Diagnostics;
using System.Data.SqlClient;
using System.Globalization;
using Microsoft.TeamFoundation.Migration.Toolkit.VC;
using Microsoft.TeamFoundation.VersionControl.Client;
using Microsoft.TeamFoundation.Client;

namespace Microsoft.TeamFoundation.Migration.VersionControl.Wss
{
    public class Tfs2WssAnalysisEngine : TfsToSourceAnalysisEngine
    {
        string m_commentModifier;
        Change[] m_reorderedChangeset;

        public Tfs2WssAnalysisEngine(VersionControlSession session, ChangeGroupingMananger manager)
            : base(session, manager)
        {
            Debug.Assert(manager.Direction == MigrationDirection.TfsToSource);

            if (manager.Direction != MigrationDirection.TfsToSource)
            {
                throw new MigrationException("Grouping manager direction must be TfsToSource");
            }

            TeamFoundationServer tfsServer = TeamFoundationServerFactory.GetServer(MigrationConfiguration.TfsServers[session.Target.Server].Server);
            tfsClient = (VersionControlServer)tfsServer.GetService(typeof(VersionControlServer));

            m_algorithms = new TfsToWssAnalysisAlgorithms(session);

            int defaultHwm = loadEarliestHwmBasedOnMappings(session);
            m_hwm = new HighWaterMark<int>(session, "TFSAnalysisHWM", defaultHwm);
            m_hwm.BeforeUpdate += m_hwm_BeforeUpdate;

            if (m_hwm.Value < defaultHwm)
            {
                m_hwm.Update(defaultHwm);
            }

            manager.LastHighWaterMark = m_hwm;

            m_commentModifier = session.GetValue<string>("MigrationChangeCommentModifier", "(Migrated by TFS Migration Toolkit)");
        }

        private int loadEarliestHwmBasedOnMappings(VersionControlSession session)
        {
            int currentOldest = tfsClient.GetLatestChangesetId();

            foreach (VersionControlMapping m in session.Mappings)
            {
                try
                {
                    // In WSS 2 deletes are destructive (i.e. the item is lost forever) so there 
                    // is no reason to default to adding items we know we are going to destroy later on.
                    bool slotmode = session.GetValue<bool>("UseSlotmodeMappings", false);

                    while (true)
                    {
                        bool foundChanges = false;
                        foreach (Changeset latest in tfsClient.QueryHistory(m.Target,
                            VersionSpec.Latest,
                            0,
                            RecursionType.None,
                            null,
                            null,
                            new ChangesetVersionSpec(currentOldest),
                            256,
                            false,
                            slotmode))
                        {
                            // we want to use the oldest starting point - if result is newer then use the older
                            if (currentOldest >= latest.ChangesetId)
                            {
                                foundChanges = true;

                                currentOldest = latest.ChangesetId;
                            }

                            /* decrement once more to ensure we go far enough back
                            * the scenario we need to be careful of is when the root directory
                            * and a child item(s) are created in the same changeset we still want 
                            * to migrate the child item(s) so we need to go to the change just prior
                            * to them.
                            * 
                            * We want to decrement wether or not we found a change because if the latest
                            * checkin is the checkin that created the migration root we still need to
                            * go down one.
                            */

                            currentOldest--;
                        }

                        if (!foundChanges)
                        {
                            break;
                        }
                    }
                }
                catch (ItemNotFoundException)
                {
                    // the root does not need to exist prior to migration
                }
            }

            return currentOldest;
        }

        void m_hwm_BeforeUpdate(object sender, HighWaterMarkUpdatedEventArgs<int> args)
        {
            int newHwm = args.New;
            int currentHwm = args.Current;

            // we only move forward
            Debug.Assert(newHwm >= currentHwm);

            // we never go negative
            Debug.Assert(newHwm >= 0);
            
            if (newHwm < currentHwm)
            {
                throw new MigrationException("High water mark should not go backwards");
            }

            if (newHwm < 0)
            {
                throw new MigrationException("High water mark must be greater than zero");
            }
        }

        protected override bool HandleMigrationException(Exception me)
        {
            if (me is VCInvalidPathException)
            {
                Trace.TraceWarning("Skipping path with invalid character: {0}", me.Message);
                return true;
            }

            return false;
        }

        protected override TfsAnalysisAlgorithms LoadAnalysisAlgorithms()
        {
            return m_algorithms;
        }

        protected override VersionControlMapping FindChangeMapping(Change change)
        {
            VersionControlMapping mapping = Util.FindAppropriateMappingForServerPath(change.Item.ServerItem, Session);
            if ((change.ChangeType == ChangeType.Rename) && (mapping == null))
            {
                warningIfRenameSourceIsMapped(change);
            }
            return mapping;
        }

        /// <summary>
        /// This method is called if a rename target is not mapped. A warning message is sent in this situation
        /// </summary>
        /// <param name="change">The rename change to be checked.</param>
        private void warningIfRenameSourceIsMapped(Change change)
        {
            if ((change == null) || (change.ChangeType != ChangeType.Rename))
            {
                return;
            }
            Item previous = change.Item.VersionControlServer.GetItem(change.Item.ItemId, change.Item.ChangesetId - 1, false);
            if (previous == null)
            {
                return;
            }
            if (Util.FindAppropriateMappingForServerPath(previous.ServerItem, Session) != null)
            {
                Session.OnAnalysisWarning(new VersionControlEventArgs(
                    string.Format(CultureInfo.InvariantCulture, "Skipping renamed item from {0} to {1}. The target item is outside session mappings",
                    previous, change.Item.ServerItem), SystemType.Tfs));
            }
        }

        protected override ChangeGroupingMethod GroupingMethod
        {
            get
            {
                return ChangeGroupingMethod.OneChangePerGroup;
            }
        }

        protected override ChangeGrouping CreateChangeGrouping(Changeset changeSet)
        {
            ChangeGrouping group = Manager.Create(changeSet.ChangesetId.ToString(CultureInfo.InvariantCulture));
            group.Owner = getOwnerName(changeSet);
            group.Comment = getComment(changeSet);
            group.ChangeTimeUtc = changeSet.CreationDate.ToUniversalTime();
            group.Status = ChangeStatus.Analysis;
            group.SessionId = Session.Id;

            return group;
        }

        private string getComment(Changeset changeSet)
        {
            return string.Format(CultureInfo.InvariantCulture, "{0} {1}", 
                changeSet.Comment, m_commentModifier);
        }

        private static string getOwnerName(Changeset changeSet)
        {
            return changeSet.Owner;
        }

        public void Run()
        {
            m_hwm.Reload();

            string skipComment = Session.GetValue<string>("SkipChangeComment", null);

            for (Changeset changeset = getNextTfsChange(); 
                changeset != null; 
                changeset = getNextTfsChange())
            {
                TraceManager.TraceInformation("Analyzing TFS change {0}", changeset.ChangesetId);

                if (TfsUtil.IsOurTfsChange(changeset, m_commentModifier))
                {
                    TraceManager.TraceInformation("Skipping mirrored change {0}", changeset.ChangesetId);

                    // skipping changeset we submitted
                    m_hwm.Update(changeset.ChangesetId);
                    continue;
                }

                if (!string.IsNullOrEmpty(skipComment))
                {
                    if (changeset.Comment != null && changeset.Comment.Contains(skipComment))
                    {
                        TraceManager.TraceInformation("Changeset {0} contains the skip comment {1}", 
                            changeset.ChangesetId,
                            skipComment);

                        // skip based on skip comment
                        m_hwm.Update(changeset.ChangesetId);
                        continue;
                    }
                }

                changeset = getChangesetWithChangeInfo(changeset);

                if (changeContainsMappedItems(changeset))
                {
                    changeset = getChangesetWithDownloadInfo(changeset);
                    int actions = Process(changeset);

                    TraceManager.TraceInformation("Created {0} actions for TFS change {1}", 
                        actions, changeset.ChangesetId);
                }
                else
                {
                    TraceManager.TraceInformation("Changeset {0} contains no mapped items", changeset.ChangesetId);
                }

                m_hwm.Update(changeset.ChangesetId);

                Manager.PromoteAnalysisToPending();
            }
        }

        /// <summary>
        /// Analyzes the TFS changeset to generate change groupings to migrate to wss
        /// </summary>
        /// <param name="changeSet">The changeset to analyze.</param>
        /// <returns>The number of changes added to the change grouping.</returns>
        public override int Process(Changeset changeSet)
        {
            ChangeGrouping group = null;
            int changeCount = 0;

            if (changeSet != null)
            {
                Change[] cyclicalRenameChanges = BreakCyclicalRenameInChangeset(changeSet);
                List<RevisedRename> renameChangeReviselist = new List<RevisedRename>();

                // Rename those items that causes cyclical rename to a unique name
                foreach (Change c in cyclicalRenameChanges)
                {
                    if (FindChangeMapping(c) != null)
                    {
                        group = CreateChangeGrouping(changeSet);
                        RevisedRename revisedRename = m_algorithms.ReviseRename(c, group);
                        renameChangeReviselist.Add(revisedRename);
                        if (group.Actions.Count> 0)
                        {
                            group.Save();
                            changeCount++;
                            Session.OnAnalysisWarning(new VersionControlEventArgs(group, string.Format(WSS2TFSResources.Culture, WSS2TFSResources.AddExtraRenameToBreakCyclicalRename
                                , ((WssMigrationItem)group.Actions[0].TargetSourceItem).CanonicalUrl, 
                                ((WssMigrationItem)group.Actions[0].TargetTargetItem).CanonicalUrl), SystemType.Tfs));
                        }
                    }
                }

                // Call base Process() to process the remaining changes in the changeset
                changeCount = changeCount + base.Process(changeSet);

                // Rename those items back to their intended target name
                foreach (RevisedRename c in renameChangeReviselist)
                {
                    group = CreateChangeGrouping(changeSet);
                    m_algorithms.RecoverRevisedRename(c, group);
                    if (group.Actions.Count > 0)
                    {
                        group.Save();
                        changeCount++;
                        Session.OnAnalysisWarning(new VersionControlEventArgs(group, string.Format(WSS2TFSResources.Culture, WSS2TFSResources.AddExtraRenameToRecoverCyclicalRename
                            , ((WssMigrationItem)group.Actions[0].TargetSourceItem).CanonicalUrl,
                            ((WssMigrationItem)group.Actions[0].TargetTargetItem).CanonicalUrl), SystemType.Tfs));

                    }
                }
            }

            return changeCount;
        }

        protected override Change[] ReorderChangeset(Changeset changeset)
        {
            return m_reorderedChangeset;
        }


        /// <summary>
        /// Reorder the changes inside a changeset for wss. The order is 
        /// 1. Add/undelete changes
        /// 2. Rename changes with child items list earlier
        /// 3. Other changes.
        /// </summary>
        /// <param name="changeset">changeset to be reordered</param>
        /// <returns>Re-ordered array of changes</returns>
        protected Change[] BreakCyclicalRenameInChangeset(Changeset changeset)
        {
            List<Change> addundeleteChangelist = new List<Change>();
            List<Change> renameChangeList = new List<Change>();
            List<Change> otherChangeList = new List<Change>();
            List<string> renameSourceList = new List<string>(); 
            List<Change> cyclicalRenameChangeList = new List<Change>();
            
            foreach (Change change in changeset.Changes)
            {
                if (((change.ChangeType & ChangeType.Add) != 0) || ((change.ChangeType & ChangeType.Undelete) != 0))
                {
                    addundeleteChangelist.Add(change);
                }
                else if ((change.ChangeType & ChangeType.Rename) != 0)
                {
                    Item previous = change.Item.VersionControlServer.GetItem(change.Item.ItemId, change.Item.ChangesetId - 1, false);
                    renameSourceList.Add(previous.ServerItem);
                    renameChangeList.Add(change);
                }
                else
                {
                    otherChangeList.Add(change);
                }
            }

            if (renameChangeList.Count > 1)
            {
                // Sort the rename list so that the sub item's rename is listed first. 
                sortRenameList(renameChangeList);
                foreach( Change change in renameChangeList)
                {
                    if (renameSourceList.Contains(change.Item.ServerItem))
                    {
                        // conflict detected
                        cyclicalRenameChangeList.Add(change); 
                    }
                    // Always remove the current item in sourceList since the current item will be renamed to a unique path
                    renameSourceList.RemoveAt(0); 
                }
            }
            
            // remove those rename changes that cause cyclical rename.
            foreach (Change renameChange in cyclicalRenameChangeList)
            {
                renameChangeList.Remove(renameChange);
            }

            addundeleteChangelist.AddRange(renameChangeList);
            addundeleteChangelist.AddRange(otherChangeList);

            m_reorderedChangeset = addundeleteChangelist.ToArray();
            return cyclicalRenameChangeList.ToArray();
        }

        private static void sortRenameList(List<Change> renameList)
        {
            if ((renameList == null) || (renameList.Count < 2))
            {
                return;
            }
            // Sort the list so that rename with longer source path is listed first. (Child item is always processed first) 
            renameList.Sort(compareRenameActionBySourcePathLength);
        }

        /// <summary>
        /// compare source path length of two rename actions. If x's source path length is smaller, x is larger than y.
        /// </summary>
        /// <param name="x">rename change 1</param>
        /// <param name="y">rename change 2</param>
        /// <returns>positive value if x's source length is smaller. negative value if y's source length is smaller.
        /// 0 if length is equal or either one is null.</returns>
        private static int compareRenameActionBySourcePathLength(Change x, Change y)
        {
            if ((x == null) || (y == null))
            {
                return 0;
            }
            Item xPrevious =  x.Item.VersionControlServer.GetItem(x.Item.ItemId, x.Item.ChangesetId - 1, false);
            Item yPrevious =  y.Item.VersionControlServer.GetItem(y.Item.ItemId, y.Item.ChangesetId - 1, false);

            return (yPrevious.ServerItem.Length - xPrevious.ServerItem.Length);
        }

        private Changeset getChangesetWithChangeInfo(Changeset changeset)
        {
            return tfsClient.GetChangeset(changeset.ChangesetId, true, false);
        }

        private Changeset getChangesetWithDownloadInfo(Changeset changeset)
        {
            return tfsClient.GetChangeset(changeset.ChangesetId, true, true);
        }

        private bool changeContainsMappedItems(Changeset changeset)
        {
            foreach (Change change in changeset.Changes)
            {
                if (Util.FindAppropriateMappingForServerPath(change.Item.ServerItem, Session) != null)
                {
                    return true;
                }
            }

            // In case we have a rename from mapped item to unmapped item, send a warning message
            foreach (Change change in changeset.Changes)
            {
                if (change.ChangeType == ChangeType.Rename)
                {
                    warningIfRenameSourceIsMapped(change);
                }
            }
            return false;
        }

        private Changeset getNextTfsChange()
        {
            int nextChange = m_hwm.Value + 1;

            while (true)
            {
                try
                {
                    return tfsClient.GetChangeset(nextChange, false, false);
                }
                catch (ChangesetNotFoundException)
                {
                    // in v1.0 this means that we have hit the end of the change chain but in v-Next
                    // there might be gaps in the sequence.  When that time comes adding a call to
                    // tfsClient.GetLatestChangesetId would go here.

                    return null;
                }
                catch (ResourceAccessException)
                {
                    // No access to any item in change
                    m_hwm.Update(nextChange);

                    // no access to any item in change
                    nextChange++;
                }
            }
        }


        VersionControlServer tfsClient;
        TfsToWssAnalysisAlgorithms m_algorithms;
        HighWaterMark<int> m_hwm;
    }

    internal class RevisedRename
    {
        private Change m_renameChange;
        private WssMigrationItem m_tempItem;

        internal RevisedRename(Change renameChange, WssMigrationItem tempItem)
        {
            m_renameChange = renameChange;
            m_tempItem = tempItem;
        }

        internal Change RenameChange
        {
            get
            {
                return m_renameChange;
            }
        }

        internal WssMigrationItem TempItem
        {
            get
            {
                return m_tempItem;
            }
        }
    }
}
